package com.googlecode.perfectable.messageable;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Set;

import com.google.common.collect.ImmutableSet;

public final class Connector {
	private final Set<Class<? extends Message>> knownTransmittedClasses;
	private final Set<Class<? extends Message>> knownReceivedClasses;
	private final DispatchingMessageProcessor processor;
	private final CompositeConnectionListener connectionListener;
	
	public static Connector create() {
		return new Connector(ImmutableSet.of(), ImmutableSet.of(), DispatchingMessageProcessor.EMPTY,
				CompositeConnectionListener.EMPTY);
	}

	private Connector(Set<Class<? extends Message>> knownTransmittedClasses,
			Set<Class<? extends Message>> knownReceivedClasses,
			DispatchingMessageProcessor processor,
			CompositeConnectionListener connectionListener) {
		this.knownTransmittedClasses = checkNotNull(knownTransmittedClasses);
		this.knownReceivedClasses = checkNotNull(knownReceivedClasses);
		this.processor = checkNotNull(processor);
		this.connectionListener = checkNotNull(connectionListener);
	}
	
	public Connector processedWith(@SuppressWarnings("hiding") MessageProcessor<Message> processor) {
		return processedWith(Message.class, processor);
	}
	
	public <M extends Message> Connector processedWith(Class<M> messageClass,
			@SuppressWarnings("hiding") MessageProcessor<M> processor) {
		DispatchingMessageProcessor newProcessor = this.processor.also(messageClass, processor);
		return new Connector(this.knownTransmittedClasses, this.knownReceivedClasses, newProcessor, this.connectionListener);
	}
	
	public Connector withTransmittedClass(Class<? extends Message> knownTransmittedClass) {
		checkNotNull(knownTransmittedClass);
		Set<Class<? extends Message>> newKnownTransmittedClasses = ImmutableSet.<Class<? extends Message>> builder()
				.addAll(this.knownTransmittedClasses).add(knownTransmittedClass).build();
		return new Connector(newKnownTransmittedClasses, this.knownReceivedClasses, this.processor, this.connectionListener);
	}

	public Connector withReceivedClass(Class<? extends Message> knownReceivedClass) {
		checkNotNull(knownReceivedClass);
		Set<Class<? extends Message>> newKnownReceivedClasses = ImmutableSet.<Class<? extends Message>> builder()
				.addAll(this.knownReceivedClasses).add(knownReceivedClass).build();
		return new Connector(this.knownTransmittedClasses, newKnownReceivedClasses, this.processor, this.connectionListener);
	}

	public Connector withConnectionListener(@SuppressWarnings("hiding") ConnectionListener connectionListener) {
		checkNotNull(connectionListener);
		CompositeConnectionListener newConnectionListener = this.connectionListener.join(connectionListener);
		return new Connector(this.knownTransmittedClasses, this.knownReceivedClasses, this.processor, newConnectionListener);
	}

	@SuppressWarnings("resource")
	public Server bind(int port) throws IOException {
		ServerSocket socket = new ServerSocket();
		socket.bind(new InetSocketAddress(port));
		ConnectionHandler handler = new ConnectionHandler() {
			@Override
			public Endpoint handleConnection(Socket clientSocket) {
				return createEndpoint(clientSocket);
			}

		};
		return new StandardServer(socket, handler);
	}
	
	@SuppressWarnings("resource")
	public Endpoint connect(String hostname, int port) throws IOException {
		Socket socket = new Socket();
		socket.connect(new InetSocketAddress(hostname, port));
		return createEndpoint(socket);
	}

	private StandardEndpoint createEndpoint(Socket socket) {
		final StandardEndpoint endpoint = new StandardEndpoint(socket, Connector.this.knownTransmittedClasses,
				Connector.this.knownReceivedClasses, Connector.this.processor);
		this.connectionListener.connectionEstablished(endpoint);
		return endpoint;
	}

}
